.. _Tutorial converting a NeurEco Regression model to a Keras model:

Tutorial: converting a NeurEco Regression model to a Keras model
==================================================================

NeurEco Tabular offers the user the possibility to convert a model to a keras model to be used with TensorFlow.

.. note:: 
   * This feature is only available for the python API.
   * This feature requires an existing installation of TensorFlow 2.x and keras.


he following section will use the test case :std:ref:`Energy consumption test case`. This test case is delivered with the NeurEco installation package.

The first step will be to load the data and build a model:

.. code-block:: python


   import numpy as np
   from NeurEco import NeurEcoTabular as Tabular

   ''' Load the training data '''
    print("Loading the training data".center(60, "*"))
    x_train = np.genfromtxt("x_train.csv", delimiter=";", skip_header=True)
    y_train = np.genfromtxt("y_train.csv", delimiter=";", skip_header=True)
    y_train = np.reshape(y_train, (-1, 1))
    ''' create a NeurEco Object to build the model'''
    print("Creating the NeurEco builder".center(60, "*"))
    builder = Tabular.Regressor()

    ''' Building the NeurEco Model '''
    builder.build(input_data=x_train, output_data=y_train,
                  # the rest of these parameters are optional
                  write_model_to="./EnergyConsumptionModel/EnergyConsumption.ednn",
                  checkpoint_address="./EnergyConsumptionModel/EnergyConsumption.checkpoint",
                  valid_percentage=33.33,
                  inputs_shifting="min_centered",
                  inputs_scaling="max_centered")

    ''' Delete the builder from memory'''
    print("Deleting the NeurEco builder".center(60, "*"))
    builder.delete()

Once the build is done, and the model is saved, let's convert it to a keras model. To do so, we first need to import the proper library:

.. code-block:: python


   from NeurEco import NeurEco2Keras

Once that's done, we will convert the ednn model to a keras model, and we will print out its summary:


.. code-block:: python


    neureco_model_path = "./EnergyConsumptionModel/EnergyConsumption.ednn"
    keras_model = NeurEco2Keras.neureco2keras(neureco_model_path)
    keras_model.summary()

.. code-block:: text


   Model: "EnergyConsumption_NeurEco_Keras_Model"
   _________________________________________________________________
   Layer (type)                 Output Shape              Param #
   =================================================================
   input (InputLayer)           [(None, 5)]               0
   _________________________________________________________________
   tf_op_layer_centeredInputs ( [(None, 5)]               0
   _________________________________________________________________
   tf_op_layer_normalizedInputs [(None, 5)]               0
   _________________________________________________________________
   adagos_gemm (AdagosGemm)     (None, 8)                 48
   _________________________________________________________________
   tf_op_layer_x1TensorActivati [(None, 8)]               0
   _________________________________________________________________
   adagos_gemm_1 (AdagosGemm)   (None, 1)                 9
   _________________________________________________________________
   tf_op_layer_outputDescaled ( [(None, 1)]               0
   _________________________________________________________________
   tf_op_layer_output (TensorFl [(None, 1)]               0
   =================================================================
   Total params: 57
   Trainable params: 57
   Non-trainable params: 0
   _________________________________________________________________


.. note::
   
   The number of links (NeurEco model) is slightly different than the number of trainable parameters (keras model). This is because the keras models are naturally fully connected, and some of the weights are present in the keras model although they are not needed (they have a value of 0).


At this stage, we can evaluate the two models (NeurEco and Keras), and see the difference between them:

.. code-block:: python

   x_test = np.genfromtxt("x_test.csv", delimiter=";")[1:, :]

   ''' evaluate the model using neureco '''
   neureco_model = Tabular.Regressor()
   neureco_model.load(neureco_model_path)
   neureco_output = neureco_model.evaluate(x_test)

   ''' evaluate the model using keras '''
   keras_output = keras_model.predict(x_test.astype("float32"))

   ''' compare the models results '''
   error = np.linalg.norm(keras_output - neureco_output) / np.linalg.norm(neureco_output)
   print("Error between the models:", error)

.. code-block:: text
   
   Error between the models: 1.6618171240269623e-07

The keras model can be saved and restored for later use. For example, we can save it as a h5 model to the disk:

.. code-block:: python

   import tensorflow as tf   
   keras_model.save("./EnergyConsumptionModel/EnergyConsumption.h5")

We can now reload it from the disk. However, a custom class is needed to perform the load. This class is called *AdagosGemm* and is in the library *NeurEco2Keras*. To load the model simply run:

.. code-block:: python

   import tensorflow as tf
   reloaded_keras_model = tf.keras.models.load_model("./EnergyConsumptionModel/EnergyConsumption.h5",
                                                     custom_objects={'AdagosGemm': NeurEco2Keras.AdagosGemm})

If the user doesn't have the possibility to import *NeurEco2keras* in its production environment, simply copy and paste the following class:

.. code-block:: python

   class AdagosGemm(tf.keras.layers.Layer):
      def __init__(self, w_init, b_init, alpha, beta, w_name, b_name, comp=False, name=None, **kwargs):
           super(AdagosGemm, self).__init__(name=name)
           self.trainable = not comp
           self.w_init = w_init
           self.b_init = b_init
           self.alpha = alpha
           self.beta = beta
           self.w_name = w_name
           self.b_name = b_name
           self.w = None
           self.b = None

      def get_config(self):
           config = super().get_config()
           config.update({"trainable": self.trainable,
                          "w_init": self.w_init,
                          "b_init": self.b_init,
                          "alpha": self.alpha,
                          "beta": self.beta,
                          "w_name": self.w_name,
                          "b_name": self.b_name,
                          "w": self.w.numpy(),
                          "b": self.b.numpy()})
           return config

      def build(self, input_shape):
           self.w = self.add_weight(shape=self.w_init,
                                    name=self.w_name, trainable=self.trainable)
           self.b = self.add_weight(shape=self.b_init,
                                    name=self.b_name, trainable=self.trainable)
           self.built = True

      def call(self, a):
           return self.alpha * tf.matmul(a, self.w) + self.beta * self.b

To load the model in this case, we will change the custom object to this class:

.. code-block:: python

   import tensorflow as tf
   reloaded_keras_model = tf.keras.models.load_model("./EnergyConsumptionModel/EnergyConsumption.h5",
                                                     custom_objects={'AdagosGemm': AdagosGemm})

We can evaluate the reloaded model on the testing data, and compare it to the neureco_outputs obtained previously:

.. code-block:: python

   ''' evaluate the model using keras '''
   reloaded_keras_output = reloaded_keras_model.predict(x_test.astype("float32"))

   ''' compare the models results '''
   error = np.linalg.norm(reloaded_keras_output - neureco_output) / np.linalg.norm(neureco_output)
   print("Error between the models (NeurEco and reloaded):", error)

.. code-block:: text
   
   Error between the models (NeurEco and reloaded): 1.6618171240269623e-07
